#include <stdlib.h>
#include <string.h>
#include <sys/cdefs.h>
#include "sdkconfig.h"

#if CONFIG_LCD_ENABLE_DEBUG_LOG
// The local log level must be defined before including esp_log.h
// Set the maximum log level for this source file
#define LOG_LOCAL_LEVEL ESP_LOG_DEBUG
#endif

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "esp_log.h"    
#include "esp_check.h"
#include "esp_lcd_types.h"
#include "esp_lcd_panel_interface.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "esp_lcd_panel_commands.h"
#include "esp_lcd_new_panel_st7735.h"

#define NO_DELAY                         0

#define HIGH_BYTE(x)                     ((uint8_t)(((x) >> 8) & 0xFF))
#define LOW_BYTE(x)                      ((uint8_t)((x) & 0xFF))

static const char* TAG = "ST7735";

static esp_err_t panel_st7735_del(esp_lcd_panel_handle_t panel);
static esp_err_t panel_st7735_reset(esp_lcd_panel_handle_t panel);
static esp_err_t panel_st7735_init(esp_lcd_panel_handle_t panel);
static esp_err_t panel_st7735_draw_bitmap(
    esp_lcd_panel_handle_t panel,
    int x_start,
    int y_start,
    int x_end,
    int y_end,
    const void* color_data
);
static esp_err_t panel_st7735_invert_color(
    esp_lcd_panel_handle_t panel,
    bool invert_color_data
);
static esp_err_t panel_st7735_mirror(
    esp_lcd_panel_handle_t panel,
    bool mirror_x,
    bool mirror_y
);
static esp_err_t panel_st7735_swap_xy(esp_lcd_panel_handle_t panel, bool swap_axes);
static esp_err_t panel_st7735_set_gap(esp_lcd_panel_handle_t panel, int x_gap, int y_gap);
static esp_err_t panel_st7735_disp_on_off(esp_lcd_panel_handle_t panel, bool off);
static esp_err_t panel_st7735_sleep(esp_lcd_panel_handle_t panel, bool sleep);

// LCD device control struct
typedef struct {
    esp_lcd_panel_t base;
    esp_lcd_panel_io_handle_t io;
    int reset_gpio_num;
    bool reset_level;
    int x_gap;
    int y_gap;
    uint8_t fb_bits_per_pixel;
    uint8_t madctl_val;    // save current value of LCD_CMD_MADCTL register
    uint8_t colmod_val;    // save current value of LCD_CMD_COLMOD register
    const st7735_init_command_t* init_commands;
    uint16_t init_commands_count;
} st7735_panel_t;

typedef st7735_panel_t* st7735_panel_handle_t;

// LCD ST7735 vendor specific device initialization commands
static const st7735_init_command_t vendor_specific_init_commands[] = {
    { LCD_CMD_SWRESET, NULL, 0, 150 },  // Software reset, 0 args, w/delay 150
    { LCD_CMD_SLPOUT, NULL, 0, 255 },   // Out of sleep mode, 0 args, w/delay 255
    { LCD_CMD_FRMCTR1, (uint8_t[]){ 0x01, 0x2C, 0x2D }, 3, NO_DELAY },  // Frame rate ctrl - normal mode, 3 args: Rate = fosc/(1x2+40) * (LINE+2C+2D) 
    { LCD_CMD_FRMCTR2, (uint8_t[]){ 0x01, 0x2C, 0x2D }, 3, NO_DELAY },  // Frame rate control - idle mode, 3 args:Rate = fosc/(1x2+40) * (LINE+2C+2D)
    { LCD_CMD_FRMCTR3, (uint8_t[]){ 0x01, 0x2C, 0x2D, 0x01, 0x2C, 0x2D }, 6, NO_DELAY },    //Frame rate ctrl - partial mode, 6 args:Dot inversion mode. Line inversion mode
    { LCD_CMD_INVCTRL, (uint8_t[]){ 0x07 }, 1, NO_DELAY },  // Display inversion ctrl, 1 arg, no delay:No inversion
    { LCD_CMD_PWRCTR1, (uint8_t[]){ 0xA2, 0x02, 0x84 }, 3, NO_DELAY },  // Power control, 3 args, no delay:-4.6V AUTO mode
    { LCD_CMD_PWRCTR2, (uint8_t[]){ 0xC5 }, 1, NO_DELAY },  // Power control, 1 arg, no delay:VGH25 = 2.4C VGSEL = -10 VGH = 3 * AVDD
    { LCD_CMD_PWRCTR3, (uint8_t[]){ 0x0A, 0x00 }, 2, NO_DELAY },    // Power control, 2 args, no delay: Opamp current small, Boost frequency
    { LCD_CMD_PWRCTR4, (uint8_t[]){ 0x8A, 0x2A }, 2, NO_DELAY },    // Power control, 2 args, no delay: BCLK/2, Opamp current small & Medium low
    { LCD_CMD_PWRCTR5, (uint8_t[]){ 0x8A, 0xEE }, 2, NO_DELAY },    // Power control, 2 args, no delay
    { LCD_CMD_VMCTRL1, (uint8_t[]){ 0x0E }, 1, NO_DELAY },  // Power control, 1 arg, no delay
    { LCD_CMD_INVOFF, NULL, 0, NO_DELAY },
    { LCD_CMD_MADCTL, (uint8_t[]){ LCD_CMD_MX_BIT | LCD_CMD_MY_BIT | LCD_CMD_BGR_BIT }, 1, NO_DELAY },
    { LCD_CMD_COLMOD, (uint8_t[]){ 0x05 }, 1, NO_DELAY },   // set color mode, 1 arg, no delay: 16-bit color
#if CONFIG_LCD_ST7735_COLOR_INVERSION_ON == 1
    { LCD_CMD_INVON, NULL, 0, NO_DELAY },
#else
    { LCD_CMD_INVOFF, NULL, 0, NO_DELAY },
#endif
#if CONFIG_LCD_ST7735_REVISION_R == 1
    { LCD_CMD_CASET, (uint8_t[]){ 0x00, 0x00, 0x00, 0x4F }, 4, NO_DELAY },
    { LCD_CMD_RASET, (uint8_t[]){ 0x00, 0x00, 0x00, 0x9F }, 4, NO_DELAY },
#elif CONFIG_LCD_ST7735_REVISION_G == 1
    { LCD_CMD_CASET, (uint8_t[]){ 0x00, 0x00, 0x00, 0x7F }, 4, NO_DELAY },
    { LCD_CMD_RASET, (uint8_t[]){ 0x00, 0x00, 0x00, 0x7F }, 4, NO_DELAY },
#endif
    { 
        LCD_CMD_GMCTRP1, 
        (uint8_t[]){ 
            0x02, 0x1C, 0x07, 0x12, 
            0x37, 0x32, 0x29, 0x2D, 
            0x29, 0x25, 0x2B, 0x39, 
            0x00, 0x01, 0x03, 0x10
        },
        16,
        NO_DELAY
    },  // 16 args, no delay:
    { 
        LCD_CMD_GMCTRN1, 
        (uint8_t[]){ 
            0x03, 0x1D, 0x07, 0x06, 
            0x2E, 0x2C, 0x29, 0x2D, 
            0x2E, 0x2E, 0x37, 0x3F, 
            0x00, 0x00, 0x02, 0x10 
        },  
        16,
        NO_DELAY
    },  // 16 args, no delay:
    { LCD_CMD_NORON, NULL, 0, 10 },     // Normal display on, no args, w/delay 10 ms delay
    { LCD_CMD_DISPON, NULL, 0, 100 }    // Main screen turn on, no args w/delay 100 ms delay
};

esp_err_t esp_lcd_new_panel_st7735(
    const esp_lcd_panel_io_handle_t io,
    const esp_lcd_panel_dev_config_t* panel_dev_config,
    esp_lcd_panel_handle_t* ret_panel)
{
    st7735_panel_handle_t st7735 = NULL;
#if CONFIG_LCD_ENABLE_DEBUG_LOG
    esp_log_level_set(TAG, ESP_LOG_DEBUG);
#endif
    esp_err_t ret = ESP_OK;

    ESP_GOTO_ON_FALSE(
        io && panel_dev_config && ret_panel,
        ESP_ERR_INVALID_ARG,
        err,
        TAG,
        "invalid argument"
    );

    st7735 = malloc(sizeof(st7735_panel_t));
    ESP_GOTO_ON_FALSE(st7735, ESP_ERR_NO_MEM, err, TAG, "no mem for st7735 panel");
    
    memset(st7735, 0x00, sizeof(st7735_panel_t));

    if (0 <= panel_dev_config->reset_gpio_num)
    {
        const gpio_config_t io_conf = {
            .mode = GPIO_MODE_OUTPUT,
            .pin_bit_mask = 1ULL << panel_dev_config->reset_gpio_num,
        };
        ESP_GOTO_ON_ERROR(gpio_config(&io_conf), err, TAG, "configure GPIO for RST line failed");
    }

#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0)
    switch (panel_dev_config->color_space)
    {
        case ESP_LCD_COLOR_SPACE_RGB:
            st7735->madctl_val = 0X00;
        break;

        case ESP_LCD_COLOR_SPACE_BGR:
            st7735->madctl_val |= LCD_CMD_BGR_BIT;
        break;

        default:
            ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space");
        break;
    }
#else
    switch (panel_dev_config->rgb_ele_order)
    {
        case LCD_RGB_ELEMENT_ORDER_RGB:
            st7735->madctl_val = LCD_CMD_MX_BIT | LCD_CMD_MY_BIT;
        break;

        case LCD_RGB_ELEMENT_ORDER_BGR:
            st7735->madctl_val = LCD_CMD_MX_BIT | LCD_CMD_MY_BIT | LCD_CMD_BGR_BIT;
        break;

        default:
            ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported color space");
        break;
    }
#endif

    uint8_t fb_bits_per_pixel = 0;
    switch (panel_dev_config->bits_per_pixel)
    {
        case 16: // RGB565
            st7735->colmod_val = 0x05;
            fb_bits_per_pixel = 16;
        break;

        case 18: // RGB666
            st7735->colmod_val = 0x66;
            // each color component (R/G/B) should occupy the 6 high bits of a byte, which means 3 full bytes are required for a pixel
            fb_bits_per_pixel = 24;
        break;

        default:
            ESP_GOTO_ON_FALSE(false, ESP_ERR_NOT_SUPPORTED, err, TAG, "unsupported pixel width");
        break;
    }

    if (panel_dev_config->vendor_config)
    {
        const st7735_vendor_config_t* vendor_config = (st7735_vendor_config_t*)panel_dev_config->vendor_config;
        st7735->init_commands = vendor_config->init_commands;
        st7735->init_commands_count = vendor_config->init_commands_count;
    }
    /*else
    {
        st7735->init_commands = NULL;
        ESP_LOGD(TAG, "No vendor config");
    }*/

    st7735->io = io;
    st7735->fb_bits_per_pixel = fb_bits_per_pixel;
    st7735->reset_gpio_num = panel_dev_config->reset_gpio_num;
    st7735->reset_level = panel_dev_config->flags.reset_active_high;
    st7735->base.del = panel_st7735_del;
    st7735->base.reset = panel_st7735_reset;
    st7735->base.init = panel_st7735_init;
    st7735->base.draw_bitmap = panel_st7735_draw_bitmap;
    st7735->base.invert_color = panel_st7735_invert_color;
    st7735->base.set_gap = panel_st7735_set_gap;
    st7735->base.mirror = panel_st7735_mirror;
    st7735->base.swap_xy = panel_st7735_swap_xy;
    st7735->base.disp_on_off = panel_st7735_disp_on_off;
    st7735->base.disp_sleep = panel_st7735_sleep;

    *ret_panel = &(st7735->base);
    
    ESP_LOGD(TAG, "new st7735 panel @%p", st7735);

    return ESP_OK;

err:
    if (st7735)
    {
        if (panel_dev_config->reset_gpio_num >= 0)
        {
            gpio_reset_pin(panel_dev_config->reset_gpio_num);
        }
        
        free(st7735);
    }

    return ret;
}

esp_err_t esp_lcd_new_panel_st7735_flush(esp_lcd_panel_handle_t panel)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    esp_lcd_panel_io_handle_t io = st7735->io;
            
    ESP_RETURN_ON_ERROR( 
        esp_lcd_panel_io_tx_param(io, LCD_CMD_NOP, NULL, 0),
        TAG,
        "flush failed"
    );

    return ESP_OK;
}

/*
 * Private LCD ST7735 panel fuctions implementation
 */

static esp_err_t panel_st7735_del(esp_lcd_panel_handle_t panel)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);

    if (0 <= st7735->reset_gpio_num)
    {
        gpio_reset_pin(st7735->reset_gpio_num);
    }

    ESP_LOGD(TAG, "del st7789 panel @%p", st7735);

    free(st7735);
    
    return ESP_OK;
}

static esp_err_t panel_st7735_reset(esp_lcd_panel_handle_t panel)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    esp_lcd_panel_io_handle_t io = st7735->io;

    // perform hardware reset
    if (0 <= st7735->reset_gpio_num)
    {
        gpio_set_level(st7735->reset_gpio_num, st7735->reset_level);
        vTaskDelay(pdMS_TO_TICKS(10));
        gpio_set_level(st7735->reset_gpio_num, !st7735->reset_level);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
    else
    { // perform software reset
        ESP_RETURN_ON_ERROR(
            esp_lcd_panel_io_tx_param(io, LCD_CMD_SWRESET, NULL, 0),
            TAG,
            "io tx param failed"
        );
        vTaskDelay(pdMS_TO_TICKS(20)); // spec, wait at least 5m before sending new command
    }

    ESP_LOGD(TAG, "Reset st7735 panel @%p", st7735);

    return ESP_OK;
}

static esp_err_t panel_st7735_init(esp_lcd_panel_handle_t panel)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    esp_lcd_panel_io_handle_t io = st7735->io;
    bool is_default_commands = st7735->init_commands == NULL;
    const st7735_init_command_t* init_commands = NULL;
    uint16_t init_commands_count;

    ESP_LOGD(TAG, "Has init commands @%p @%p %d", st7735, st7735->init_commands, is_default_commands);

    if (is_default_commands)
    {
        init_commands = vendor_specific_init_commands;
        init_commands_count = sizeof(vendor_specific_init_commands) / sizeof(st7735_init_command_t);
    }
    else
    {
        init_commands = st7735->init_commands;
        init_commands_count = st7735->init_commands_count;
    }

    for (uint16_t index = 0; init_commands_count > index; index++)
    {
        switch (init_commands[index].lcd_cmd)
        {
            case LCD_CMD_MADCTL:
                if (is_default_commands)
                {
                    ESP_RETURN_ON_ERROR(
                        esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]){st7735->madctl_val}, 1),
                        TAG,
                        "io tx param failed"
                    );
                    continue;
                }
            break;

            case LCD_CMD_COLMOD:
                if (is_default_commands)
                {
                    ESP_RETURN_ON_ERROR(
                        esp_lcd_panel_io_tx_param(io, LCD_CMD_COLMOD, (uint8_t[]){st7735->colmod_val}, 1),
                        TAG,
                        "io tx param failed"
                    );
                    continue;
                }
            break;
        
            default:
                break;
        }

        ESP_RETURN_ON_ERROR(
            esp_lcd_panel_io_tx_param(io, init_commands[index].lcd_cmd, init_commands[index].param, init_commands[index].param_size),
            TAG,
            "io tx param failed"
        );

        if (NO_DELAY < init_commands[index].delay_ms)
        {
            vTaskDelay(pdMS_TO_TICKS(init_commands[index].delay_ms));
        }
    }

    ESP_LOGD(TAG, "new st7735 panel @%p initialized", st7735);

    return ESP_OK;
}

static esp_err_t panel_st7735_draw_bitmap(
    esp_lcd_panel_handle_t panel,
    int x_start,
    int y_start,
    int x_end,
    int y_end,
    const void* color_data)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    assert((x_start < x_end) && (y_start < y_end) && "start position must be smaller than end position");
    esp_lcd_panel_io_handle_t io = st7735->io;

    x_start += st7735->x_gap;
    x_end += st7735->x_gap;
    y_start += st7735->y_gap;
    y_end += st7735->y_gap;

    // define an area of frame memory where MCU can access
    ESP_RETURN_ON_ERROR(
        esp_lcd_panel_io_tx_param(io, LCD_CMD_CASET, (uint8_t[]) {
            HIGH_BYTE(x_start), LOW_BYTE(x_start),
            HIGH_BYTE(x_end - 1), LOW_BYTE(x_end - 1),
        }, 4),
        TAG,
        "io tx param failed"
    );
    ESP_RETURN_ON_ERROR(
        esp_lcd_panel_io_tx_param(io, LCD_CMD_RASET, (uint8_t[]) {
            HIGH_BYTE(y_start), LOW_BYTE(y_start),
            HIGH_BYTE(y_end - 1), LOW_BYTE(y_end - 1),
        }, 4),
        TAG,
        "io tx param failed"
    );

    // transfer frame buffer
    size_t length = (x_end - x_start) * (y_end - y_start) * (st7735->fb_bits_per_pixel >> 3);
    ESP_RETURN_ON_ERROR(
        esp_lcd_panel_io_tx_color(io, LCD_CMD_RAMWR, color_data, length),
        TAG,
        "io tx color failed"
    );

    return ESP_OK;
}

static esp_err_t panel_st7735_invert_color(
    esp_lcd_panel_handle_t panel,
    bool invert_color_data)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    esp_lcd_panel_io_handle_t io = st7735->io;    
    int command = 0;

    if (invert_color_data)
    {
        command = LCD_CMD_INVON;
    }
    else
    {
        command = LCD_CMD_INVOFF;
    }
    
    ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "io tx param failed");

    return ESP_OK;
}

static esp_err_t panel_st7735_mirror(
    esp_lcd_panel_handle_t panel,
    bool mirror_x,
    bool mirror_y)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    esp_lcd_panel_io_handle_t io = st7735->io;

    if (mirror_x)
    {
        st7735->madctl_val |= LCD_CMD_MX_BIT;
    }
    else
    {
        st7735->madctl_val &= ~LCD_CMD_MX_BIT;
    }

    if (mirror_y)
    {
        st7735->madctl_val |= LCD_CMD_MY_BIT;
    }
    else
    {
        st7735->madctl_val &= ~LCD_CMD_MY_BIT;
    }

    ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) {
        st7735->madctl_val
    }, 1), TAG, "io tx param failed");

    return ESP_OK;
}

static esp_err_t panel_st7735_swap_xy(esp_lcd_panel_handle_t panel, bool swap_axes)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    esp_lcd_panel_io_handle_t io = st7735->io;

    if (swap_axes)
    {
        st7735->madctl_val |= LCD_CMD_MV_BIT;
    }
    else
    {
        st7735->madctl_val &= ~LCD_CMD_MV_BIT;
    }

    ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, LCD_CMD_MADCTL, (uint8_t[]) {
        st7735->madctl_val
    }, 1), TAG, "io tx param failed");

    return ESP_OK;
}

static esp_err_t panel_st7735_set_gap(
    esp_lcd_panel_handle_t panel,
    int x_gap,
    int y_gap)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    
    st7735->x_gap = x_gap;
    st7735->y_gap = y_gap;
    
    return ESP_OK;
}

static esp_err_t panel_st7735_disp_on_off(
    esp_lcd_panel_handle_t panel,
    bool on_off)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    esp_lcd_panel_io_handle_t io = st7735->io;
    int command = on_off ? LCD_CMD_DISPON : LCD_CMD_DISPOFF;
    
    ESP_RETURN_ON_ERROR(esp_lcd_panel_io_tx_param(io, command, NULL, 0), TAG, "io tx param failed");
    
    return ESP_OK;
}

static esp_err_t panel_st7735_sleep(esp_lcd_panel_handle_t panel, bool sleep)
{
    st7735_panel_handle_t st7735 = __containerof(panel, st7735_panel_t, base);
    esp_lcd_panel_io_handle_t io = st7735->io;
    const int command = sleep ? LCD_CMD_SLPIN : LCD_CMD_SLPOUT;

    ESP_RETURN_ON_ERROR(
        esp_lcd_panel_io_tx_param(io, command, NULL, 0),
        TAG,
        "io tx param failed"
    );

    vTaskDelay(pdMS_TO_TICKS(100));

    return ESP_OK;
}
